/*
 * @(#)TreeSearchable.java
 *
 * Copyright 2002 - 2004 JIDE Software Inc. All rights reserved.
 */
package com.element.swing.search;

import javax.swing.*;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;

/**
 * <code>TreeSearchable</code> is an concrete implementation of {@link Searchable} that enables the search function in
 * JTree. <p>It's very simple to use it. Assuming you have a JTree, all you need to do is to call
 * <code><pre>
 * JTree tree = ....;
 * TreeSearchable searchable = new TreeSearchable(tree);
 * </pre></code>
 * Now the JTree will have the search function.
 * <p/>
 * There is very little customization you need to do to TreeSearchable. The only thing you might need is when the
 * element in the JTree needs a special conversion to convert to string. If so, you can override
 * convertElementToString() to provide you own algorithm to do the conversion.
 * <code><pre>
 * JTree tree = ....;
 * TreeSearchable searchable = new TreeSearchable(tree) {
 *      protected String convertElementToString(Object object) {
 *          ...
 *      }
 * };
 * </pre></code>
 * <p/>
 * Additional customization can be done on the base Searchable class such as background and foreground color,
 * keystrokes, case sensitivity. <p/> JTree actually has a simple searchable feature but has flaws. It will affect our
 * searchable feature. To workaround it, you can override getNextMatch method and always return -1 when you create your
 * JList. <code>
 * <pre>
 * JTree tree = new JTree(...) {
 *    public TreePath getNextMatch(String prefix, int startingRow, Position.Bias bias) {
 *        return null;
 *    }
 * };
 * </pre>
 * </code>
 */
public class TreeSearchable extends Searchable implements TreeModelListener, PropertyChangeListener {

	private boolean _recursive = false;
	private transient List<TreePath> _treePathes;

	public TreeSearchable(JTree tree) {
		super(tree);
		if (tree.getModel() != null) {
			tree.getModel().addTreeModelListener(this);
		}

		tree.addPropertyChangeListener(JTree.TREE_MODEL_PROPERTY, this);
	}

	/**
	 * Checks if the searchable is recursive.
	 *
	 * @return true if searchable is recursive.
	 */
	public boolean isRecursive() {
		return _recursive;
	}

	/**
	 * Sets the recursive attribute.
	 * <p/>
	 * If TreeSearchable is recursive, it will consider all tree nodes including those which are not visible to find the matching
	 * node. Obviously, if your tree has unlimited number of tree nodes or a potential huge number of tree nodes (such
	 * as a tree to represent file system), the recursive attribute should be false. To avoid this potential problem in
	 * this case, we default it to false.
	 *
	 * @param recursive the attribute
	 */
	public void setRecursive(boolean recursive) {
		_recursive = recursive;
		resetTreePathes();
	}

	@Override
	public void uninstallListeners() {
		super.uninstallListeners();
		if (_component instanceof JTree) {
			if (((JTree) _component).getModel() != null) {
				((JTree) _component).getModel().removeTreeModelListener(this);
			}
		}
		_component.removePropertyChangeListener(JTree.TREE_MODEL_PROPERTY, this);
	}

	@Override
	public void setSelectedIndex(int index, boolean incremental) {
		if (!isRecursive()) {
			if (incremental) {
				((JTree) _component).addSelectionInterval(index, index);
			} else {
				((JTree) _component).setSelectionRow(index);
			}
			((JTree) _component).scrollRowToVisible(index);
		} else {
			Object elementAt = getElementAt(index);
			if (elementAt instanceof TreePath path) { // else case should never happen
				if (incremental) {
					((JTree) _component).addSelectionPath(path);
				} else {
					((JTree) _component).setSelectionPath(path);
				}
				((JTree) _component).scrollPathToVisible(path);
			}
		}
	}

	@Override
	public int getSelectedIndex() {
		if (!isRecursive()) {
			int[] ai = ((JTree) _component).getSelectionRows();
			return (ai != null && ai.length != 0) ? ai[0] : -1;
		} else {
			TreePath[] treePaths = ((JTree) _component).getSelectionPaths();
			if (treePaths != null && treePaths.length > 0) {
				return getTreePathes().indexOf(treePaths[0]);
			} else
				return -1;
		}
	}

	@Override
	public Object getElementAt(int index) {
		if (index == -1) {
			return null;
		}
		if (!isRecursive()) {
			return ((JTree) _component).getPathForRow(index);
		} else {
			return getTreePathes().get(index);
		}
	}

	@Override
	public int getElementCount() {
		if (!isRecursive()) {
			return ((JTree) _component).getRowCount();
		} else {
			return getTreePathes().size();
		}
	}

	/**
	 * Recursively go through the tree to populate the tree paths into a list and cache them.
	 * <p/>
	 * Tree paths list is only used when recursive attribute is true.
	 */
	protected void populateTreePaths() {
		_treePathes = new ArrayList<>();
		Object root = ((JTree) _component).getModel().getRoot();
		populateTreePaths0(root, new TreePath(root), ((JTree) _component).getModel());
	}

	private void populateTreePaths0(Object node, TreePath path, TreeModel model) {
		if (((JTree) _component).isRootVisible() || path.getLastPathComponent() != ((JTree) _component).getModel().getRoot()) {
			// if root not visible, do not add root
			_treePathes.add(path);
		}
		for (int i = 0; i < model.getChildCount(node); i++) {
			Object childNode = model.getChild(node, i);
			populateTreePaths0(childNode, path.pathByAddingChild(childNode), model);
		}
	}

	/**
	 * Reset the cached tree paths list.
	 * <p/>
	 * Tree paths list is only used when recursive atattributes true.
	 */
	protected void resetTreePathes() {
		_treePathes = null;
	}

	/**
	 * Gets the cached tree paths list. If it has never been cached before, this method will create the cache.
	 * <p/>
	 * Tree paths list is only used when recursive atattributes true.
	 *
	 * @return the tree paths list.
	 */
	protected List<TreePath> getTreePathes() {
		if (_treePathes == null) {
			populateTreePaths();
		}
		return _treePathes;
	}

	/**
	 * Converts the element in JTree to string. The element by default is TreePath. The returned value will be
	 * <code>toString()</code> of the last path component in the TreePath.
	 *
	 * @param object the object to be converted
	 * @return the string representing the TreePath in the JTree.
	 */
	@Override
	public String convertElementToString(Object object) {
		if (object instanceof TreePath) {
			Object treeNode = ((TreePath) object).getLastPathComponent();
			if (getComponent() instanceof JTree tree) {
				TreePath[] selectionPaths = tree.getSelectionPaths();
				boolean selected = false;
				if (selectionPaths != null) {
					for (TreePath selectedPath : selectionPaths) {
						if (selectedPath == object) {
							selected = true;
							break;
						}
					}
				}
				return tree.convertValueToText(treeNode, selected, tree.isExpanded((TreePath) object), tree.getModel().isLeaf(treeNode), tree.getRowForPath((TreePath) object), tree.hasFocus() && tree.getLeadSelectionPath() == object);
			}
			return treeNode.toString();
		} else if (object != null) {
			return object.toString();
		} else {
			return "";
		}
	}

	public void treeNodesChanged(TreeModelEvent e) {
		if (!isProcessModelChangeEvent()) {
			return;
		}
		hidePopup();
		resetTreePathes();
	}

	public void treeNodesInserted(TreeModelEvent e) {
		if (!isProcessModelChangeEvent()) {
			return;
		}
		hidePopup();
		resetTreePathes();
	}

	public void treeNodesRemoved(TreeModelEvent e) {
		if (!isProcessModelChangeEvent()) {
			return;
		}
		hidePopup();
		resetTreePathes();
	}

	public void treeStructureChanged(TreeModelEvent e) {
		if (!isProcessModelChangeEvent()) {
			return;
		}
		hidePopup();
		resetTreePathes();
	}

	public void propertyChange(PropertyChangeEvent evt) {
		if (JTree.TREE_MODEL_PROPERTY.equals(evt.getPropertyName())) {
			hidePopup();

			if (evt.getOldValue() instanceof TreeModel) {
				((TreeModel) evt.getOldValue()).removeTreeModelListener(this);
			}

			if (evt.getNewValue() instanceof TreeModel) {
				((TreeModel) evt.getNewValue()).addTreeModelListener(this);
			}

			resetTreePathes();
		}
	}
}
